The goal of this notebook is to implement your own boosting module.
Brace yourselves! This is going to be a fun and challenging assignment.
Let's get started!
Make sure you have the latest version of GraphLab Create (1.8.3 or newer). Upgrade by
pip install graphlab-create --upgrade
See this page for detailed instructions on upgrading.
import graphlab as gl
print('gl.version: %s' % (gl.version))
gl.canvas.set_target('ipynb')
import matplotlib.pyplot as plt
%matplotlib inline
# my imports
import math
import numpy as np
import pandas as pd
from six.moves import cPickle as pickle
import string
We will be using the same LendingClub dataset as in the previous assignment.
#loans = gl.SFrame('lending-club-data.gl/')
glbObsAll = gl.load_sframe('data/module-6-decision-tree-practical-assignment_glbObsAll.gl')
print(glbObsAll.shape)
glbObsAll.show()
print(glbObsAll)
We will now repeat some of the feature processing steps that we saw in the previous assignment:
First, we re-assign the target to have +1 as a safe (good) loan, and -1 as a risky (bad) loan.
Next, we select four categorical features:
features = ['grade', # grade of the loan
'term', # the term of the loan
'home_ownership', # home ownership status: own, mortgage or rent
'emp_length', # number of years of employment
]
# loans['safe_loans'] = loans['bad_loans'].apply(lambda x : +1 if x==0 else -1)
# loans.remove_column('bad_loans')
target = 'safe_loan'
#loans = loans[features + [target]]
glbObsAll, glbObsAll_with_na = glbObsAll[[target] + features].dropna_split()
# Count the number of rows with missing data
num_rows_with_na = glbObsAll_with_na.num_rows()
num_rows = glbObsAll.num_rows()
print 'Dropping %s observations; keeping %s ' % (num_rows_with_na, num_rows)
Just as we did in the previous assignment, we will undersample the larger class (safe loans) in order to balance out our dataset. This means we are throwing away many data points. We use seed=1 so everyone gets the same results.
# safe_loans_raw = loans[loans[target] == 1]
# risky_loans_raw = loans[loans[target] == -1]
# # Undersample the safe loans.
# percentage = len(risky_loans_raw)/float(len(safe_loans_raw))
# risky_loans = risky_loans_raw
# safe_loans = safe_loans_raw.sample(percentage, seed=1)
# glbObsSmp = risky_loans_raw.append(safe_loans)
# print "Percentage of safe loans :", len(safe_loans) / float(len(glbObsSmp))
# print "Percentage of risky loans :", len(risky_loans) / float(len(glbObsSmp))
# print "Total number of loans in our new dataset :", len(glbObsSmp)
glbObsSfe = glbObsAll[glbObsAll[target] == +1]
glbObsRsk = glbObsAll[glbObsAll[target] == -1]
# Since there are less risky loans than safe loans, find the ratio of the sizes
# and use that percentage to undersample the safe loans.
percentage = len(glbObsRsk)/float(len(glbObsSfe))
glbObsSfeSmp = glbObsSfe.sample(percentage, seed = 1)
#risky_glbObsAll = glbObsRsk
glbObsSmp = glbObsRsk.append(glbObsSfeSmp)
print "Percentage of safe loans : %.2f%%" % \
(len(glbObsSfeSmp) * 100.0 / float(len(glbObsSmp)))
print "Percentage of risky loans : %.2f%%" % \
(len(glbObsRsk) * 100.0 / float(len(glbObsSmp)))
print "Total number of loans in our new dataset :", len(glbObsSmp)
Note: There are many approaches for dealing with imbalanced data, including some where we modify the learning algorithm. These approaches are beyond the scope of this course, but some of them are reviewed in this paper. For this assignment, we use the simplest possible approach, where we subsample the overly represented class to get a more balanced dataset. In general, and especially when the data is highly imbalanced, we recommend using more advanced methods.
In this assignment, we will work with binary decision trees. Since all of our features are currently categorical features, we want to turn them into binary features using 1-hot encoding.
We can do so with the following code block (see the first assignments for more details):
#glbObsSmp = risky_loans.append(safe_loans)
for feature in features:
glbObsSmp_one_hot_encoded = glbObsSmp[feature].apply(lambda x: {x: 1})
glbObsSmp_unpacked = glbObsSmp_one_hot_encoded.unpack(column_name_prefix=feature)
# Change None's to 0's
for column in glbObsSmp_unpacked.column_names():
glbObsSmp_unpacked[column] = glbObsSmp_unpacked[column].fillna(0)
glbObsSmp.remove_column(feature)
glbObsSmp.add_columns(glbObsSmp_unpacked)
Let's see what the feature columns look like now:
features = glbObsSmp.column_names()
features.remove(target) # Remove the response variable
features
We split the data into training and test sets with 80% of the data in the training set and 20% of the data in the test set. We use seed=1 so that everyone gets the same result.
#glbObsFit, glbObsOOB = glbObsSmp.random_split(0.8, seed=1)
glbObsFit, glbObsOOB = glbObsSmp.random_split(.8, seed=1)
print(glbObsFit.shape)
print(glbObsOOB.shape)
Let's modify our decision tree code from Module 5 to support weighting of individual data points.
Consider a model with $N$ data points with:
Then the weighted error is defined by: $$ \mathrm{E}(\mathbf{\alpha}, \mathbf{\hat{y}}) = \frac{\sum_{i=1}^{n} \alpha_i \times 1[y_i \neq \hat{y_i}]}{\sum_{i=1}^{n} \alpha_i} $$ where $1[y_i \neq \hat{y_i}]$ is an indicator function that is set to $1$ if $y_i \neq \hat{y_i}$.
Write a function that calculates the weight of mistakes for making the "weighted-majority" predictions for a dataset. The function accepts two inputs:
nodeLabels: Targets $y_1 ... y_n$ obsWeights: Data point weights $\alpha_1 ... \alpha_n$We are interested in computing the (total) weight of mistakes, i.e. $$ \mathrm{WM}(\mathbf{\alpha}, \mathbf{\hat{y}}) = \sum_{i=1}^{n} \alpha_i \times 1[y_i \neq \hat{y_i}]. $$ This quantity is analogous to the number of mistakes, except that each mistake now carries different weight. It is related to the weighted error in the following way: $$ \mathrm{E}(\mathbf{\alpha}, \mathbf{\hat{y}}) = \frac{\mathrm{WM}(\mathbf{\alpha}, \mathbf{\hat{y}})}{\sum_{i=1}^{n} \alpha_i} $$
The function getWgtdMistakesIntermediateNode should first compute two weights:
$\mathrm{WM}_{+1}$: weight of mistakes when all predictions are $\hat{y}_i = +1$ i.e $\mbox{WM}(\mathbf{\alpha}, \mathbf{+1}$)
where $\mathbf{-1}$ and $\mathbf{+1}$ are vectors where all values are -1 and +1 respectively.
After computing $\mathrm{WM}_{-1}$ and $\mathrm{WM}_{+1}$, the function getWgtdMistakesIntermediateNode should return the lower of the two weights of mistakes, along with the class associated with that weight. We have provided a skeleton for you with YOUR CODE HERE to be filled in several places.
def getWgtdMistakesIntermediateNode(nodeLabels, obsWeights):
# Sum the weights of all entries with label +1
obsPveWeightsSum = sum(obsWeights[nodeLabels == +1])
# Weight of mistakes for predicting all -1's is equal to the sum above
### YOUR CODE HERE
wgtdMistakesAllNve = obsPveWeightsSum
# Sum the weights of all entries with label -1
### YOUR CODE HERE
obsNveWeightsSum = sum(obsWeights[nodeLabels == -1])
# Weight of mistakes for predicting all +1's is equal to the sum above
### YOUR CODE HERE
wgtdMistakesAllPve = obsNveWeightsSum
# Return the tuple (weight, class_label) representing the lower of the two weights
# class_label should be an integer of value +1 or -1.
# If the two weights are identical, return (wgtdMistakesAllPve, +1)
### YOUR CODE HERE
if (wgtdMistakesAllPve <= wgtdMistakesAllNve):
return((wgtdMistakesAllPve, +1))
else:
return((wgtdMistakesAllNve, -1))
Checkpoint: Test your getWgtdMistakesIntermediateNode function, run the following cell:
example_labels = gl.SArray([-1, -1, 1, 1, 1])
example_obsWeights = gl.SArray([1., 2., .5, 1., 1.])
if getWgtdMistakesIntermediateNode(example_labels, example_obsWeights) == (2.5, -1):
print 'Test passed!'
else:
print 'Test failed... try again!'
Recall that the classification error is defined as follows: $$ \mbox{classification error} = \frac{\mbox{# mistakes}}{\mbox{# all data points}} $$
Quiz Question: If we set the weights $\mathbf{\alpha} = 1$ for all data points, how is the weight of mistakes $\mbox{WM}(\mathbf{\alpha}, \mathbf{\hat{y}})$ related to the classification error?
We continue modifying our decision tree code from the earlier assignment to incorporate weighting of individual data points. The next step is to pick the best feature to split on.
The getFeatureSplitBest function is similar to the one from the earlier assignment with two minor modifications:
obsWeights to take account of weights of data points.Complete the following function. Comments starting with DIFFERENT HERE mark the sections where the weighted version differs from the original implementation.
# If the data is identical in each feature, this function should return None
def getFeatureSplitBest(data, features, target, obsWeights):
# These variables will keep track of the best feature and the corresponding error
featureBest = None
errorBest = float('+inf')
nObs = float(len(data))
# Loop through each feature to consider splitting on that feature
for feature in features:
# The left split will have all data points where the feature value is 0
# The right split will have all data points where the feature value is 1
splitLft = data[data[feature] == 0]
splitRgt = data[data[feature] != 0]
# Apply the same filtering to obsWeights to create obsWeightsLft, obsWeightsRgt
## YOUR CODE HERE
obsWeightsLft = obsWeights[data[feature] == 0]
obsWeightsRgt = obsWeights[data[feature] != 0]
# DIFFERENT HERE
# Calculate the weight of mistakes for left and right sides
## YOUR CODE HERE
wgtdMistakesLft, classLft = \
getWgtdMistakesIntermediateNode(splitLft[target], obsWeightsLft)
wgtdMistakesRgt, classRgt = \
getWgtdMistakesIntermediateNode(splitRgt[target], obsWeightsRgt)
# DIFFERENT HERE
# Compute weighted error by computing
# ( [weight of mistakes (left)] + [weight of mistakes (right)] ) /
# [total weight of all data points]
## YOUR CODE HERE
error = (wgtdMistakesLft + wgtdMistakesRgt) / sum(obsWeights)
# If this is the best error we have found so far, store the feature and the error
if error < errorBest:
featureBest = feature
errorBest = error
# Return the best feature we found
return featureBest
Checkpoint: Now, we have another checkpoint to make sure you are on the right track.
example_obsWeights = gl.SArray(len(glbObsFit)* [1.5])
if getFeatureSplitBest(glbObsFit, features, target, example_obsWeights) == 'term. 36 months':
print 'Test passed!'
else:
print 'Test failed... try again!'
Note. If you get an exception in the line of "the logical filter has different size than the array", try upgradting your GraphLab Create installation to 1.8.3 or newer.
Very Optional. Relationship between weighted error and weight of mistakes
By definition, the weighted error is the weight of mistakes divided by the weight of all data points, so $$ \mathrm{E}(\mathbf{\alpha}, \mathbf{\hat{y}}) = \frac{\sum_{i=1}^{n} \alpha_i \times 1[y_i \neq \hat{y_i}]}{\sum_{i=1}^{n} \alpha_i} = \frac{\mathrm{WM}(\mathbf{\alpha}, \mathbf{\hat{y}})}{\sum_{i=1}^{n} \alpha_i}. $$
In the code above, we obtain $\mathrm{E}(\mathbf{\alpha}, \mathbf{\hat{y}})$ from the two weights of mistakes from both sides, $\mathrm{WM}(\mathbf{\alpha}_{\mathrm{left}}, \mathbf{\hat{y}}_{\mathrm{left}})$ and $\mathrm{WM}(\mathbf{\alpha}_{\mathrm{right}}, \mathbf{\hat{y}}_{\mathrm{right}})$. First, notice that the overall weight of mistakes $\mathrm{WM}(\mathbf{\alpha}, \mathbf{\hat{y}})$ can be broken into two weights of mistakes over either side of the split: $$ \mathrm{WM}(\mathbf{\alpha}, \mathbf{\hat{y}}) = \sum{i=1}^{n} \alpha_i \times 1[y_i \neq \hat{y_i}] = \sum{\mathrm{left}} \alpha_i \times 1[y_i \neq \hat{y_i}]
With the above functions implemented correctly, we are now ready to build our decision tree. Recall from the previous assignments that each node in the decision tree is represented as a dictionary which contains the following keys:
{
'isLeaf' : True/False.
'prediction' : Prediction at the leaf node.
'lft' : (dictionary corresponding to the left tree).
'rgt' : (dictionary corresponding to the right tree).
'featureRemaining' : List of features that are posible splits.
}
Let us start with a function that creates a leaf node given a set of target values:
def bldLeaf(targetVctr, obsWeights):
# Create a leaf node
leaf = {'featureSplit' : None,
'isLeaf' : True}
# Computed weight of mistakes.
errorWgtd, classBest = getWgtdMistakesIntermediateNode(targetVctr, obsWeights)
# Store the predicted class (1 or -1) in leaf['prediction']
leaf['prediction'] = classBest ## YOUR CODE HERE
return leaf
We provide a function that learns a weighted decision tree recursively and implements 3 stopping conditions:
def bldWgtdDTree(data, features, target, obsWeights, depthCur = 1, depthMax = 10):
featureRemaining = features[:] # Make a copy of the features.
targetVctr = data[target]
print "--------------------------------------------------------------------"
print "Subtree, depth = %s (%s data points)." % (depthCur, len(targetVctr))
# Stopping condition 1. Error is 0.
if getWgtdMistakesIntermediateNode(targetVctr, obsWeights)[0] <= 1e-15:
print "Stopping condition 1 reached."
return bldLeaf(targetVctr, obsWeights)
# Stopping condition 2. No more features.
if featureRemaining == []:
print "Stopping condition 2 reached."
return bldLeaf(targetVctr, obsWeights)
# Additional stopping condition (limit tree depth)
if depthCur > depthMax:
print "Reached maximum depth. Stopping for now."
return bldLeaf(targetVctr, obsWeights)
# If all the datapoints are the same, featureSplit will be None. Create a leaf
featureSplit = getFeatureSplitBest(data, features, target, obsWeights)
featureRemaining.remove(featureSplit)
splitLft = data[data[featureSplit] == 0]
splitRgt = data[data[featureSplit] != 0]
obsWeightsLft = obsWeights[data[featureSplit] == 0]
obsWeightsRgt = obsWeights[data[featureSplit] != 0]
print "Split on feature %s. (%s, %s)" % (\
featureSplit, len(splitLft), len(splitRgt))
# Create a leaf node if the split is "perfect"
if len(splitLft) == len(data):
print "Creating leaf node."
return bldLeaf(splitLft[target], obsWeights)
if len(splitRgt) == len(data):
print "Creating leaf node."
return bldLeaf(splitRgt[target], obsWeights)
# Repeat (recurse) on left and right subtrees
treeLft = bldWgtdDTree(
splitLft, featureRemaining, target, obsWeightsLft, depthCur + 1, depthMax)
treeRgt = bldWgtdDTree(
splitRgt, featureRemaining, target, obsWeightsRgt, depthCur + 1, depthMax)
return {'isLeaf' : False,
'prediction' : None,
'featureSplit' : featureSplit,
'lft' : treeLft,
'rgt' : treeRgt}
Here is a recursive function to count the nodes in your tree:
def getNNodes(tree):
if tree['isLeaf']:
return 1
return 1 + getNNodes(tree['lft']) + getNNodes(tree['rgt'])
Run the following test code to check your implementation. Make sure you get 'Test passed' before proceeding.
example_obsWeights = gl.SArray([1.0 for i in range(len(glbObsFit))])
smallDataWgtdDTree = bldWgtdDTree(glbObsFit, features, target,
example_obsWeights, depthMax=2)
if getNNodes(smallDataWgtdDTree) == 7:
print 'Test passed!'
else:
print 'Test failed... try again!'
print 'Number of nodes found:', getNNodes(smallDataWgtdDTree)
print 'Number of nodes that should be there: 7'
Let us take a quick look at what the trained tree is like. You should get something that looks like the following
{'isLeaf': False,
'lft': {'isLeaf': False,
'lft': {'isLeaf': True, 'prediction': -1, 'featureSplit': None},
'prediction': None,
'rgt': {'isLeaf': True, 'prediction': 1, 'featureSplit': None},
'featureSplit': 'grade.A'
},
'prediction': None,
'rgt': {'isLeaf': False,
'lft': {'isLeaf': True, 'prediction': 1, 'featureSplit': None},
'prediction': None,
'rgt': {'isLeaf': True, 'prediction': -1, 'featureSplit': None},
'featureSplit': 'grade.D'
},
'featureSplit': 'term. 36 months'
}
smallDataWgtdDTree
We give you a function that classifies one data point. It can also return the probability if you want to play around with that as well.
def classifyWgtdDTree(tree, x, annotate = False):
# If the node is a leaf node.
if tree['isLeaf']:
if annotate:
print "At leaf, predicting %s" % tree['prediction']
return tree['prediction']
else:
# Split on feature.
split_feature_value = x[tree['featureSplit']]
if annotate:
print "Split on %s = %s" % (tree['featureSplit'], split_feature_value)
if split_feature_value == 0:
return classifyWgtdDTree(tree['lft'], x, annotate)
else:
return classifyWgtdDTree(tree['rgt'], x, annotate)
Now, we will write a function to evaluate a decision tree by computing the classification error of the tree on the given dataset.
Again, recall that the classification error is defined as follows: $$ \mbox{classification error} = \frac{\mbox{# mistakes}}{\mbox{# all data points}} $$
The function called evlClassificationError takes in as input:
tree (as described above)data (an SFrame)The function does not change because of adding data point weights.
def evlClassificationError(tree, data):
# Apply the classifyWgtdDTree(tree, x) to each row in your data
prediction = data.apply(lambda x: classifyWgtdDTree(tree, x))
# Once you've made the predictions, calculate the classification error
return (prediction != data[target]).sum() / float(len(data))
evlClassificationError(smallDataWgtdDTree, glbObsOOB)
To build intuition on how weighted data points affect the tree being built, consider the following:
Suppose we only care about making good predictions for the first 10 and last 10 items in glbObsFit, we assign weights:
Let us fit a weighted decision tree with depthMax = 2.
# Assign weights
example_obsWeights = gl.SArray([1.] * 10 + [0.]*(len(glbObsFit) - 20) + [1.] * 10)
# Train a weighted decision tree model.
smallDataSubset20WgtdDTree = bldWgtdDTree(glbObsFit, features, target,
example_obsWeights, depthMax=2)
Now, we will compute the classification error on the subset20ObsFit, i.e. the subset of data points whose weight is 1 (namely the first and last 10 data points).
subset20ObsFit = glbObsFit.head(10).append(glbObsFit.tail(10))
evlClassificationError(smallDataSubset20WgtdDTree, subset20ObsFit)
Now, let us compare the classification error of the model smallDataSubset20WgtdDTree on the entire test set glbObsFit:
evlClassificationError(smallDataSubset20WgtdDTree, glbObsFit)
The model smallDataSubset20WgtdDTree performs a lot better on subset20ObsFit than on glbObsFit.
So, what does this mean?
Quiz Question: Will you get the same model as smallDataSubset20WgtdDTree if you trained a decision tree with only the 20 data points with non-zero weights from the set of points in subset20ObsFit?
Now that we have a weighted decision tree working, it takes only a bit of work to implement Adaboost. For the sake of simplicity, let us stick with decision tree stumps by training trees with depthMax=1.
Recall from the lecture the procedure for Adaboost:
1. Start with unweighted data with $\alpha_j = 1$
2. For t = 1,...T:
Complete the skeleton for the following code to implement bldAdaboostDTreeStumps. Fill in the places with YOUR CODE HERE.
from math import log
from math import exp
def bldAdaboostDTreeStumps(data, features, target, nDTreeStumps):
# start with unweighted data
alpha = gl.SArray([1.]*len(data))
weights = []
curDTreeStumps = []
targetVctr = data[target]
for t in xrange(nDTreeStumps):
print '====================================================='
print 'Adaboost Iteration %d' % t
print '====================================================='
# Learn a weighted decision tree stump. Use depthMax=1
thsDTreeStump = bldWgtdDTree(data, features, target, obsWeights=alpha, depthMax=1)
curDTreeStumps.append(thsDTreeStump)
# Make predictions
predictions = data.apply(lambda x: classifyWgtdDTree(thsDTreeStump, x))
# Produce a Boolean array indicating whether
# each data point was correctly classified
isAccurate = predictions == targetVctr
isInAccurt = predictions != targetVctr
# Compute weighted error
# YOUR CODE HERE
errorWgtd = sum(alpha[isInAccurt]) / sum(alpha)
# Compute model coefficient using weighted error
# YOUR CODE HERE
weight = (1.0 / 2.0) * log((1 - errorWgtd) / errorWgtd)
weights.append(weight)
# Adjust weights on data point
adjustment = isAccurate.apply(lambda isAccurate :
exp(-weight) if isAccurate else exp(weight))
# Scale alpha by multiplying by adjustment
# Then normalize data points weights
## YOUR CODE HERE
alpha = alpha * adjustment
alpha = alpha / sum(alpha)
return weights, curDTreeStumps
Train an ensemble of two tree stumps and see which features those stumps split on. We will run the algorithm with the following parameters:
glbObsFitfeaturestargetnDTreeStumps = 2wgtStumps, curDTreeStumps = bldAdaboostDTreeStumps(glbObsFit, features, target, nDTreeStumps=2)
def dspDTreeStump(tree):
split_name = tree['featureSplit'] # split_name is something like 'term. 36 months'
if split_name is None:
print "(leaf, label: %s)" % tree['prediction']
return None
split_feature, split_value = split_name.split('.')
print ' root'
print ' |---------------|----------------|'
print ' | |'
print ' | |'
print ' | |'
print ' [{0} == 0]{1}[{0} == 1] '.format(split_name, ' '*(27-len(split_name)))
print ' | |'
print ' | |'
print ' | |'
print ' (%s) (%s)' \
% (('leaf, label: ' + str(tree['lft']['prediction']) if tree['lft']['isLeaf'] else 'subtree'),
('leaf, label: ' + str(tree['rgt']['prediction']) if tree['rgt']['isLeaf'] else 'subtree'))
Here is what the first stump looks like:
dspDTreeStump(curDTreeStumps[0])
Here is what the next stump looks like:
dspDTreeStump(curDTreeStumps[1])
print wgtStumps
If your Adaboost is correctly implemented, the following things should be true:
curDTreeStumps[0] should split on term. 36 months with the prediction -1 on the left and +1 on the right.curDTreeStumps[1] should split on grade.A with the prediction -1 on the left and +1 on the right.[0.158, 0.177] Reminders
Let us train an ensemble of 10 decision tree stumps with Adaboost. We run the bldAdaboostDTreeStumps function with the following parameters:
glbObsFitfeaturestargetnDTreeStumps = 10wgtStumps, curDTreeStumps = bldAdaboostDTreeStumps(glbObsFit, features,
target, nDTreeStumps=10)
Recall from the lecture that in order to make predictions, we use the following formula: $$ \hat{y} = sign\left(\sum_{t=1}^T \hat{w}_t f_t(x)\right) $$
We need to do the following things:
wgtStumps with the predictions $f_t(x)$ from the decision treesComplete the following skeleton for making predictions:
def predictAdaboost(wgtStumps, curDTreeStumps, data):
scores = gl.SArray([0.]*len(data))
for i, thsDTreeStump in enumerate(curDTreeStumps):
predictions = data.apply(lambda x: classifyWgtdDTree(thsDTreeStump, x))
# Accumulate predictions on scores array
# YOUR CODE HERE
scores += wgtStumps[i] * predictions
return scores.apply(lambda score : +1 if score > 0 else -1)
predictions = predictAdaboost(wgtStumps, curDTreeStumps, glbObsOOB)
accuracy = gl.evaluation.accuracy(glbObsOOB[target], predictions)
print 'Accuracy of 10-component ensemble = %s' % accuracy
Now, let us take a quick look what the wgtStumps look like at the end of each iteration of the 10-stump ensemble:
wgtStumps
Quiz Question: Are the weights monotonically decreasing, monotonically increasing, or neither?
Reminder: Stump weights ($\mathbf{\hat{w}}$) tell you how important each stump is while making predictions with the entire boosted ensemble.
In this section, we will try to reproduce some of the performance plots dicussed in the lecture.
We will now train an ensemble with:
glbObsFitfeaturestargetnDTreeStumps = 30Once we are done with this, we will then do the following:
First, lets train the model.
# this may take a while...
wgtStumps, curDTreeStumps = bldAdaboostDTreeStumps(glbObsFit,
features, target, nDTreeStumps=30)
Now, we will compute the classification error on the glbObsFit and see how it is reduced as trees are added.
errorIter = []
for n in xrange(1, 31):
predictions = predictAdaboost(wgtStumps[:n], curDTreeStumps[:n], glbObsFit)
error = 1.0 - gl.evaluation.accuracy(glbObsFit[target], predictions)
errorIter.append(error)
print "Iteration %s, training error = %s" % (n, errorIter[n-1])
We have provided you with a simple code snippet that plots classification error with the number of iterations.
plt.rcParams['figure.figsize'] = 7, 5
plt.plot(range(1,31), errorIter, '-', linewidth=4.0, label='Training error')
plt.title('Performance of Adaboost ensemble')
plt.xlabel('# of iterations')
plt.ylabel('Classification error')
plt.legend(loc='best', prop={'size':15})
plt.rcParams.update({'font.size': 16})
Quiz Question: Which of the following best describes a general trend in accuracy as we add more and more components? Answer based on the 30 components learned so far.
Performing well on the training data is cheating, so lets make sure it works on the glbObsOOB as well. Here, we will compute the classification error on the glbObsOOB at the end of each iteration.
errorIterOOB = []
for n in xrange(1, 31):
predictions = predictAdaboost(wgtStumps[:n], curDTreeStumps[:n], glbObsOOB)
error = 1.0 - gl.evaluation.accuracy(glbObsOOB[target], predictions)
errorIterOOB.append(error)
print "Iteration %s, test error = %s" % (n, errorIterOOB[n-1])
Now, let us plot the training & test error with the number of iterations.
plt.rcParams['figure.figsize'] = 7, 5
plt.plot(range(1,31), errorIter, '-', linewidth=4.0, label='Training error')
plt.plot(range(1,31), errorIterOOB, '-', linewidth=4.0, label='Test error')
plt.title('Performance of Adaboost ensemble')
plt.xlabel('# of iterations')
plt.ylabel('Classification error')
plt.rcParams.update({'font.size': 16})
plt.legend(loc='best', prop={'size':15})
plt.tight_layout()
Quiz Question: From this plot (with 30 trees), is there massive overfitting as the # of iterations increases?